/*  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#include "luge-node.hh"
#include "luge-color.hh"
#include "luge-util.hh"
#include "luge-pad.hh"
#include "luge-logging.hh"
#include <gegl-utils.h>
#include <iostream>
#include <vector>
#include <sstream>
#include <string>

namespace {

/*
 * gegl-nodes are opaque pointers. Within lua, use a user-data object
 * containing only the pointer.
 */
static const char mtname_node[] = "-gegl:node";

/*
 * metafunctions for the node type.
 */
static int node_gc (lua_State *ls);
static int node_call (lua_State *ls);
static int node_str (lua_State *ls);
static int node_newindex (lua_State *ls);
static int node_index (lua_State *ls);

/*
 * these two are methods for node objects to be called from lua.
 */
static int node_process (lua_State *ls);
static int node_save (lua_State *ls);
static int node_sample (lua_State *ls);
static int node_bounding_box (lua_State *ls);

/**
 * push the metatable for nodes onto the stack.
 */
static void push_node_metatable (lua_State *ls)
{
  if (luaL_newmetatable (ls, mtname_node)) {
    const int node_table = lua_absindex (ls, -1);
    lua_pushcclosure (ls, node_gc, 0);
    lua_setfield (ls, node_table, "__gc");
    lua_pushcclosure (ls, node_call, 0);
    lua_setfield (ls, node_table, "__call");
    lua_pushcclosure (ls, node_str, 0);
    lua_setfield (ls, node_table, "__tostring");
    lua_pushcclosure (ls, node_newindex, 0);
    lua_setfield (ls, node_table, "__newindex");
    lua_pushcclosure (ls, node_index, 0);
    lua_setfield (ls, node_table, "__index");
  }
}

/**
 * push a rectangle as a table with keys x,y,w,h.
 */
static void push_rect (lua_State *ls, const GeglRectangle &rect)
{
  lua_createtable (ls, 0, 4);
  const int tbl_idx = lua_gettop (ls);
  lua_pushnumber (ls, rect.x); lua_setfield (ls, tbl_idx, "x");
  lua_pushnumber (ls, rect.y); lua_setfield (ls, tbl_idx, "y");
  lua_pushnumber (ls, rect.width); lua_setfield (ls, tbl_idx, "width");
  lua_pushnumber (ls, rect.height); lua_setfield (ls, tbl_idx, "height");
}

/**
 * function to get the size of a node.
 */
static int node_bounding_box (lua_State *ls)
{
  GeglNode *node = luge::to_node (ls, 1);
  GeglRectangle rect = gegl_node_get_bounding_box (node);
  push_rect (ls, rect);
  return 1;
}

/**
 * convert a node to a string.
 */
static int node_str (lua_State *ls)
{
  GeglNode *node = luge::to_node (ls, 1);
  std::string str (luge::node_tostring (node));
  lua_pushstring (ls, str.c_str ());
  return 1;
}

/**
 * garbage collect the node.
 */
static int node_gc (lua_State *ls)
{
  GeglNode *node = luge::to_node (ls, 1);
  g_object_unref (node);
  return 0;
}

/**
 * get the named attribute from the node and push a usable lua value
 * onto the top of the stack. This pushes the value as a lua object if
 * possible.
 * returns the number of items pushed (usually 1).
 */
static int node_push_key_property
  (GeglNode *node, const std::string &key, lua_State *ls)
{
  const char *ckey = key.c_str ();
  /*
   * check for io-pads first, because some io-pads also identify themselves
   * as properties.
   */
  if (luge::is_io_prop (node, key)) {
    /*
     * properties that are pads are handled differently.
     */
    if (luge::is_output_prop (node, key)) {
      /*
       * create a direct value for this output pad.
       */
      luge::push_pad (ls, luge::pad (node, key));
    } else {
      /*
       * for inputs, create a reference for the connected output.
       */
      gchar *output_pad_name = 0;
      GeglNode *source_node = gegl_node_get_producer
        (node, const_cast <gchar *> (key.c_str ()), &output_pad_name);
      if (source_node != 0) {
        luge::push_pad (ls, luge::pad (source_node, output_pad_name));
        g_free (output_pad_name);
      } else {
        lua_pushnil (ls);
      }
    }
    return 1;
  } else {
    /*
     * no pad, test for a normal property.
     */
    GParamSpec *spec = gegl_node_find_property (node, ckey);
    if (spec == 0) {
      luaL_error (ls, "no such property '%s'.", ckey);
      return 0;
    }
    GValue value = G_VALUE_INIT;
    g_value_init (&value, spec->value_type);
    gegl_node_get_property (node, ckey, &value);
    int st = luge::gvalue_push (value, ls);
    g_value_reset (&value);
    if (!st) {
      luaL_error (ls, "unhandled property type for '%s'.", key.c_str ());
      return 0;
    } else {
      return st;
    }
  }
}


/**
 * set a property value of a node.
 * the property name is given in key,
 * the value is on the stack at index val_idx.
 */
static void node_set_key_property
  (GeglNode *node, lua_State *ls, const std::string &key, int val_idx)
{
  const char *const ckey = key.c_str ();
  if (luge::is_io_prop (node, key)) {
    if (luge::is_output_prop (node, key)) {
      /*
       * currently we cannot set an output
       * (i.e. user wanted to do node.output = ....;
       */
      luaL_error (ls, "cannot assign output '%s'.", key.c_str ());
    } else {
      /*
       * an input pad.
       */
    luge::pad output_pad = luge::to_pad (ls, val_idx);
    gegl_node_connect_from
      (node, ckey, output_pad.node, output_pad.name.c_str ());
    }
  } else {
    GParamSpec *spec = gegl_node_find_property (node, ckey);
    if (spec == 0) {
      luaL_error (ls, "no such property '%s' to set.", ckey);
      /* todo: error */
      return;
    }
    if (spec->value_type == G_TYPE_STRING) {
      std::string value (lua_tostring (ls, val_idx));
      gegl_node_set (node, ckey, value.c_str (), (char *) 0);
    } else if (spec->value_type == G_TYPE_DOUBLE) {
      double value = lua_tonumber (ls, val_idx);
      gegl_node_set (node, ckey, value, (char *) 0);
    } else if (spec->value_type == G_TYPE_INT) {
      int value = lua_tonumber (ls, val_idx);
      gegl_node_set (node, ckey, value, (char *) 0);
    } else if (spec->value_type == GEGL_TYPE_COLOR) {
      GeglColor *value = luge::to_color (ls, val_idx);
      gegl_node_set (node, ckey, value, (char *) 0);
    } else {
      luaL_error (ls, "setting of property '%s' not implemented.", ckey);
    }
  }
}
static void node_set_lua_property
  (GeglNode *node, lua_State *ls, int key_idx, int val_idx)
{
  if (lua_type (ls, key_idx) != LUA_TSTRING) {
    /* todo: error */
    return;
  }
  std::string key (lua_tostring (ls, key_idx));
  node_set_key_property (node, ls, key, val_idx);
}

/**
 * retrieve an attribute of a node and push its value onto
 * the stack; handles the node-methods.
 */
static int node_index (lua_State *ls)
{
  std::string attr (luaL_checkstring (ls, 2));
  if (attr == "bounding_box") {
    lua_pushcclosure (ls, node_bounding_box, 0);
  } else if (attr == "save") {
    lua_pushcclosure (ls, node_save, 0);
  } else if (attr == "sample") {
    lua_pushcclosure (ls, node_sample, 0);
  } else if (attr == "process") {
    lua_pushcclosure (ls, node_process, 0);
  } else {
    GeglNode *const node = luge::to_node (ls, 1);
    int values_pushed = node_push_key_property (node, attr, ls);
    if (values_pushed == 0) {
      luaL_error (ls, "node has no '%s'.", attr.c_str ());
    }
  }
  return 1;
}

/**
 * helper function to create a png-save node.
 */
static GeglNode *create_save_node
  (GeglNode *input, const std::string &filename)
{
  GeglNode *output = gegl_node_new ();
  gegl_node_set (output, "operation", "gegl:save", (char *) 0);
  gegl_node_set (output, "path", filename.c_str (), (char *) 0);
  gegl_node_connect_from (output, "input", input, "output");
  return output;
}

/**
 * perform a process operation on a node.
 */
static int node_process (lua_State *ls)
{
  GeglNode *node = luge::to_node (ls, 1);
  lg::info () << "info: processing\n";
  gegl_node_process (node);
  return 0;
}

static double get_double_value
  (lua_State *ls, int tbl_idx, const std::string &key, double def)
{
  double value;
  lua_getfield (ls, tbl_idx, key.c_str ());
  if (lua_isnil (ls, -1)) {
    value = def;
  } else {
    value = lua_tonumber (ls, -1);
  }
  lua_pop (ls, 1);
  return value;
}
static std::string get_string_value
  (lua_State *ls, int tbl_idx, const std::string &key, const std::string &def)
{
  std::string value;
  lua_getfield (ls, tbl_idx, key.c_str ());
  if (lua_isnil (ls, -1)) {
    value = def;
  } else {
    value = lua_tostring (ls, -1);
  }
  lua_pop (ls, 1);
  return value;
}

/**
 * create a gegl-rectangle from the table argument at index idx.
 */
static GeglRectangle rectangle_from_table (lua_State *ls, int idx)
{
  GeglRectangle rect;
  rect.x = get_double_value (ls, idx, "x", 0);
  rect.y = get_double_value (ls, idx, "y", 0);
  rect.width = get_double_value (ls, idx, "width", 1);
  rect.height = get_double_value (ls, idx, "height", 1);
  return rect;
}

/**
 * sample a rectangular area from the node.
 * arguments are the node and a rectangle.
 */
static int node_sample (lua_State *ls)
{
  typedef std::vector <double> dbl_vec;
  typedef std::vector <float> float_vec;
  GeglNode *node = luge::to_node (ls, 1);
  GeglRectangle rect = rectangle_from_table (ls, 2);
  /*
   * create the babl-format. The data format is always "float".
   * The parameter "model" defines the colormodel.
   */
  const Babl *format;
  {
    std::string color_model (get_string_value (ls, 2, "model", "RGBA"));
    format = babl_format ((color_model + " float").c_str ());
  }
  const unsigned n_channels = babl_format_get_n_components (format);
  const unsigned n_data_points = n_channels * rect.width * rect.height;
  float_vec data (n_data_points);
  gegl_node_blit
    (node, 1, &rect, format, &data[0], GEGL_AUTO_ROWSTRIDE, GEGL_BLIT_DEFAULT);
  dbl_vec channel_sum (n_channels, 0.0);
  unsigned pos = 0;
  while (pos < n_data_points) {
    channel_sum[pos % n_channels] += data[pos];
    ++ pos;
  }
  /*
   * transform the channels to a table result.
   */
  lua_createtable (ls, n_channels, 0);
  const int result_idx = lua_absindex (ls, -1);
  unsigned idx = 0;
  while (idx != channel_sum.size ()) {
    lua_pushnumber (ls, channel_sum[idx] / (rect.width * rect.height));
    lua_rawseti (ls, result_idx, idx + 1);
    ++ idx;
  }
  return 1;
}

/**
 * save a node. This function takes a node and a filename and creates
 * a save-node for the filename.
 */
static int node_save (lua_State *ls)
{
  GeglNode *node = luge::to_node (ls, 1);
  std::string filename (luaL_checkstring (ls, 2));
  /*
   * only call process on defined bounds.
   */
  GeglRectangle rect = gegl_node_get_bounding_box (node);
  if (gegl_rectangle_is_infinite_plane (&rect)) {
    luaL_error (ls, "cannot process infinite rectangle.");
  } else {
    lg::info () << "info: rendering area "
                << std::noshowpos << rect.width << "x" << rect.height
                << std::showpos << rect.x << std::showpos << rect.y
                << '\n';
  }
  GeglNode *save_node = create_save_node (node, filename);
  gegl_node_process (save_node);
  g_object_unref (save_node);
  return 0;
}



/*
 * assignment of an attribute, like an input:
 * node.input = node
 */
static int node_newindex (lua_State *ls)
{
  GeglNode *dst = luge::to_node (ls, 1);
  int key_idx = lua_absindex (ls, 2);
  int val_idx = lua_absindex (ls, 3);
  node_set_lua_property (dst, ls, key_idx, val_idx);
  return 0;
}

/**
 * call a node as a function.
 */
static int node_call (lua_State *ls)
{
  GeglNode *node = luge::to_node (ls, 1);
  int tbl_idx;
  if (lua_gettop (ls) == 1) {
    /*
     * no arguments; simply clone the node. Check the arguments first,
     * before cloning the node unnecessarily.
     */
    tbl_idx = 0;
  } else {
    tbl_idx = lua_absindex (ls, 2);
    if (lua_type (ls, tbl_idx) != LUA_TTABLE) {
      luaL_error (ls, "argument to node must be a table.");
    }
  }
  GeglNode *clone = luge::clone_node (node);
  if (tbl_idx != 0) {
    /*
     * iterate over all key-value pairs.
     */
    lua_pushnil (ls);
    while (lua_next (ls, tbl_idx)) {
      node_set_lua_property (clone, ls, -2, -1);
      lua_pop (ls, 1);
    }
  }
  luge::push_node (ls, clone);
  return 1;
}

} /* local namespace */

/**
 * test if the object is a gegl-node.
 */
bool luge::is_node (lua_State *ls, int idx)
{
  return luaL_testudata (ls, idx, mtname_node) != 0;
}

/**
 * convert the object to a gegl-node.
 */
GeglNode *luge::to_node (lua_State *ls, int idx)
{
  GeglNode *node = *(GeglNode **) luaL_checkudata (ls, idx, mtname_node);
  return node;
}

/**
 * push a node onto the stack.
 */
void luge::push_node (lua_State *ls, GeglNode *node)
{
  /*
   * make a copy of the node within a lua-userdata.
   */
  new (lua_newuserdata (ls, sizeof node)) GeglNode * (node);
  push_node_metatable (ls);
  lua_setmetatable (ls, -2);
  g_object_ref (node);
}

/**
 * create a new gegl-node. The only argument is the op-name.
 */
int luge::node_construct (lua_State *ls)
{
  std::string opname (luaL_checkstring (ls, 1));
  GeglNode *node = gegl_node_new ();
  gegl_node_set (node, "operation", opname.c_str (), (char *) 0);
  push_node (ls, node);
  return 1;
}

